package net.lgy.service.impl;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.model.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lgy.component.StoreEngine;
import net.lgy.config.MinioConfig;
import net.lgy.controller.req.FileChunkMergeRequest;
import net.lgy.controller.req.FileChunkinitTaskRequest;
import net.lgy.controller.req.FileUploadRequest;
import net.lgy.dto.FileChunkDTO;
import net.lgy.enums.BizCodeEnum;
import net.lgy.exception.BizException;
import net.lgy.mapper.FileChunkMapper;
import net.lgy.mapper.StorageMapper;
import net.lgy.model.FileChunkDO;
import net.lgy.model.StorageDO;
import net.lgy.service.FileChunkService;
import net.lgy.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;

import java.net.URL;
import java.util.*;


@Service
@Slf4j
public class FileChunkServiceImpl implements FileChunkService {

    @Resource
    private StorageMapper storageMapper ; // 存储空间Mapper，用于查询和更新用户存储空间信息
    @Autowired
    private StoreEngine storeEngine; // 存储引擎接口，封装了与对象存储相关的操作
    @Autowired
    private FileChunkMapper fileChunkMapper; // 分片任务Mapper，用于数据库操作

    @Autowired
    private MinioConfig minioConfig; // MinIO配置类，包含桶名称、URL过期时间等配置
    @Autowired
    private AccountFileServiceImpl fileService; // 文件服务实现类，用于保存文件信息


    /**
     * 初始化大文件分片上传任务
     * 功能：创建分片上传任务，生成uploadId，记录任务信息到数据库
     *
     * @param request 初始化分片任务的请求参数，包含文件名、总大小、分片大小等信息
     * @return FileChunkDTO 包含分片任务信息的DTO对象
     */
    @Override
    public FileChunkDTO initFileChunkTask(FileChunkinitTaskRequest request) {
        // 1. 检查用户存储空间是否充足（预校验，实际扣减在合并时执行）
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>()
                .eq("account_no", request.getAccountNo())); // 根据用户账号查询存储空间信息

        // 若预估使用空间超过总空间，抛出存储空间不足异常
        if (storageDO.getUsedSize() + request.getTotalSize() > storageDO.getTotalSize()) {
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }

        // 2. 生成文件在对象存储中的唯一路径（包含文件名）
        String objectKey = CommonUtil.getFilePath(request.getFileName());

        // 3. 确定文件的MIME类型（如image/jpeg、application/pdf等）
        String contentType = MediaTypeFactory.getMediaType(objectKey)
                .orElse(MediaType.APPLICATION_OCTET_STREAM) // 默认为二进制流
                .toString();

        // 4. 配置文件元数据（主要用于指定文件类型）
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(contentType);

        // 5. 调用存储引擎初始化分片上传任务，获取uploadId（分片任务唯一标识）
        InitiateMultipartUploadResult uploadResult = storeEngine.initMultipartUploadTask(
                minioConfig.getBucketName(), // 存储桶名称
                objectKey,                  // 文件存储路径
                metadata                    // 文件元数据
        );
        String uploadId = uploadResult.getUploadId();

        // 6. 计算总分片数量（向上取整，确保最后一个分片也能被包含）
        int chunkNum = (int) Math.ceil(request.getTotalSize() * 1.0 / request.getChunkSize());

        // 7. 构建分片任务实体，准备存入数据库
        FileChunkDO task = new FileChunkDO();
        task.setBucketName(minioConfig.getBucketName())       // 存储桶名称
                .setChunkNum(chunkNum)                            // 总分片数量
                .setChunkSize(request.getChunkSize())             // 每个分片大小
                .setFileName(request.getFileName())               // 原始文件名
                .setIdentifier(request.getIdentifier())           // 文件唯一标识（通常是文件MD5）
                .setObjectKey(objectKey)                          // 对象存储中的文件路径
                .setTotalSize(request.getTotalSize())             // 文件总大小
                .setUploadId(uploadId)                            // 分片任务ID
                .setAccountNo(request.getAccountNo())             // 用户账号
                .setGmtCreate(new Date())                         // 创建时间
                .setGmtModified(new Date());                      // 修改时间

        // 8. 将任务信息保存到数据库
        fileChunkMapper.insert(task);

        // 9. 构建并返回DTO对象，标记未完成状态，初始化为空分片列表
        return new FileChunkDTO(task).setFinished(false).setExitPartList(new ArrayList<>());
    }

    /**
     * 生成文件分片上传预签名URL
     * 根据用户账号、文件唯一标识（identifier）和分片编号，生成带有效期的PUT请求URL，用于前端直传分片到存储引擎
     *
     * @param accountNo   用户账号ID（用于校验分片任务归属，确保权限）
     * @param identifier  文件唯一标识（通常是文件MD5或自定义唯一ID，关联分片任务）
     * @param partNumber  分片编号（从1开始，标识当前上传的是第几个分片）
     * @return String 分片上传预签名URL（前端可通过PUT请求直接上传分片到该URL）
     */
    @Override
    public String getFileChunkUploadUrl(Long accountNo, String identifier, int partNumber) {
        // 1. 查询数据库，获取当前用户该文件的分片任务信息
        // 核心逻辑：通过"用户账号+文件唯一标识"定位分片任务，确保任务归属正确
        FileChunkDO fileChunkDO = fileChunkMapper.selectOne(
                new QueryWrapper<FileChunkDO>()
                        .eq("account_no", accountNo)  // 权限过滤：仅查询当前用户的分片任务
                        .eq("identifier", identifier)  // 业务过滤：定位具体文件的分片任务
        );

        // 2. 校验分片任务是否存在：若不存在，说明未创建分片任务或任务已过期，抛出业务异常
        if (fileChunkDO == null) {
            log.error("用户[{}]获取分片上传URL失败：分片任务不存在，identifier=[{}]", accountNo, identifier);
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
        }

        // 3. 计算预签名URL的过期时间：从配置获取秒单位有效期，转换为毫秒后计算过期时间点
        // 关键修正：minioConfig.getPreSingUrlExpireSeconds()返回秒，需*1000转为毫秒，匹配DateUtil.offsetMillisecond参数要求
        long expireSeconds = minioConfig.getPreSingUrlExpireSeconds();
        long expireMilliseconds = expireSeconds * 1000; // 秒 → 毫秒转换
        DateTime expirationTime = DateUtil.offsetMillisecond(
                new Date(),          // 基准时间：当前系统时间
                (int) expireMilliseconds // 偏移量：配置的有效期（毫秒）
        );
        log.debug("用户[{}]分片上传URL有效期配置：{}秒（{}毫秒），过期时间：{}",
                accountNo, expireSeconds, expireMilliseconds, expirationTime);

        // 4. 准备分片上传所需的额外参数：MinIO分片上传需携带uploadId（分片任务ID）和partNumber（分片编号）
        Map<String, Object> params = new HashMap<>(2);
        params.put("uploadId", fileChunkDO.getUploadId());  // 分片任务唯一ID（MinIO创建分片任务时返回）
        params.put("partNumber", partNumber);               // 当前分片编号（用于MinIO标识分片顺序）

        try {
            // 5. 调用存储引擎生成预签名URL：抽象化存储操作，便于后续切换存储实现（如从MinIO切换到S3）
            URL presignedUrl = storeEngine.genePreSignedUrl(
                    fileChunkDO.getBucketName(),  // 存储桶名称（从分片任务中获取，支持不同文件存不同桶）
                    fileChunkDO.getObjectKey(),   // 文件在存储引擎中的唯一路径（如：2025/09/08/xxx-xxx-xxx.jpg）
                    HttpMethod.PUT,               // 请求方法：分片上传用PUT（向指定URL写入分片数据）
                    expirationTime,               // URL过期时间点
                    params                        // 分片上传额外参数（uploadId和partNumber）
            );

            log.info("用户[{}]生成分片上传URL成功：identifier=[{}]，partNumber=[{}]，URL=[{}]",
                    accountNo, identifier, partNumber, presignedUrl);
            return presignedUrl.toString();

        } catch (Exception e) {
            // 6. 捕获存储引擎异常，封装为业务异常并日志记录，便于问题排查
            log.error("用户[{}]生成分片上传URL失败：identifier=[{}]，partNumber=[{}]",
                    accountNo, identifier, partNumber, e);
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS.getCode(), "生成分片上传链接失败，请重试");
        }
    }


    /**
     * 合并所有已上传的分片为完整文件
     * 功能：校验分片完整性，更新用户存储空间，执行合并操作，保存文件最终信息
     *
     * @param request 合并分片的请求参数，包含用户账号、文件标识等
     * @return Boolean 合并是否成功
     */
    @Override
    public Boolean mergeFileChunk(FileChunkMergeRequest request) {
        // 1. 查询数据库，获取分片任务信息
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>()
                .eq("account_no", request.getAccountNo())
                .eq("identifier", request.getIdentifier()));

        // 若任务不存在，抛出文件不存在异常
        if (task == null) {
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
        }

        // 2. 查询对象存储中已上传的分片列表
        PartListing partListing = storeEngine.listMultipart(
                minioConfig.getBucketName(),  // 存储桶名称
                task.getObjectKey(),          // 文件存储路径
                task.getUploadId()            // 分片任务ID
        );
        List<PartSummary> parts = partListing.getParts();

        // 3. 校验已上传分片数量是否与预期一致
        if (parts.size() != task.getChunkNum()) {
            throw new BizException(BizCodeEnum.FILE_CHUNK_NOT_ENOUGH); // 分片数量不足
        }

        // 4. 更新用户存储空间（实际扣减）
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>()
                .eq("account_no", request.getAccountNo()));

        // 计算所有分片的总大小（即文件实际大小）
        long totalFileSize = parts.stream()
                .map(PartSummary::getSize)
                .mapToLong(Long::longValue)
                .sum();

        // 更新已使用空间
        storageDO.setUsedSize(storageDO.getUsedSize() + totalFileSize);
        storageMapper.updateById(storageDO);

        // 5. 调用存储引擎合并分片
        CompleteMultipartUploadResult result = storeEngine.mergeChunks(
                task.getBucketName(),  // 存储桶名称
                task.getObjectKey(),   // 文件存储路径
                task.getUploadId(),    // 分片任务ID
                // 转换分片信息为PartETag列表（包含分片编号和ETag）
                parts.stream()
                        .map(partSummary -> new PartETag(
                                partSummary.getPartNumber(),  // 分片编号
                                partSummary.getETag()         // 分片校验标识
                        ))
                        .toList()
        );

        // 6. 若合并成功，保存文件最终信息到数据库
        if (result.getETag() != null) {
            // 构建文件上传请求参数
            FileUploadRequest fileUploadRequest = new FileUploadRequest();
            fileUploadRequest.setAccountNo(request.getAccountNo())
                    .setFileName(task.getFileName())
                    .setIdentifier(task.getIdentifier())
                    .setFileSize(task.getTotalSize())
                    .setFile(null)  // 无需文件流，已通过分片上传
                    .setParentId(request.getParentId());  // 文件父目录ID

            // 保存文件信息及用户关联关系
            fileService.saveFileAndAccountFile(fileUploadRequest, task.getObjectKey());

            // 删除分片任务记录（清理临时数据）
            int delete = fileChunkMapper.delete(new QueryWrapper<FileChunkDO>()
                    .eq("account_no", request.getAccountNo())
                    .eq("identifier", request.getIdentifier()));

            log.info("文件分片合并完成，任务记录已删除:{}", delete);
            return true;
        }

        // 合并失败返回false
        return false;
    }


    /**
     * 查询分片上传进度
     * 功能：获取已上传的分片列表，判断是否可以合并，返回上传进度信息
     *
     * @param accountNo  用户账号
     * @param identifier 文件唯一标识（MD5）
     * @return FileChunkDTO 包含上传进度和分片信息的DTO
     */
    @Override
    public FileChunkDTO listFileChunk(Long accountNo, String identifier) {
        // 1. 查询数据库，获取分片任务信息
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>()
                .eq("account_no", accountNo)
                .eq("identifier", identifier));

        // 若任务不存在，抛出文件不存在异常
        if (task == null) {
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
        }

        // 2. 构建返回的DTO对象（基础信息从任务中获取）
        FileChunkDTO result = new FileChunkDTO(task);

        // 3. 检查对象存储中是否已存在完整文件（判断是否已合并）
        boolean objectExist = storeEngine.doesObjectExist(
                task.getBucketName(),
                task.getObjectKey()
        );

        // 4. 若文件不存在，说明还在上传中，查询已上传的分片
        if (!objectExist) {
            PartListing partListing = storeEngine.listMultipart(
                    task.getBucketName(),
                    task.getObjectKey(),
                    task.getUploadId()
            );

            // 判断已上传分片数量是否等于总数量（是否可以合并）
            if (partListing.getParts().size() == task.getChunkNum()) {
                result.setFinished(true).setExitPartList(partListing.getParts()); // 可合并
            } else {
                result.setFinished(false).setExitPartList(partListing.getParts()); // 不可合并
            }
        }

        // 返回包含进度信息的DTO
        return result;
    }
}